// START HTTP.js

/*
 *
Copyright (c) 2008, R. Alexander Milowski
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the R. Alexander Milowski nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 *
 */

var _HTTP_HEADER_NAME = new RegExp("^([a-zA-Z0-9_-]+):");
var _ioService=Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
var _authenticationManager = Components.classes["@mozilla.org/network/http-auth-manager;1"].getService(Components.interfaces.nsIHttpAuthManager);

function _HTTP_parseHeaders(headerText)
{
   var headers = {};
   if (headerText) {
      var eol = headerText.indexOf("\n");
      while (eol>=0) {
         var line = headerText.substring(0,eol);
         headerText = headerText.substring(eol+1);
         while (headerText.length>0 && !headerText.match(_HTTP_HEADER_NAME)) {
            eol = headerText.indexOf("\n");
            var nextLine = eol<0 ? headerText : headerText.substring(0,eol);
            line = line+' '+nextLine;
            headerText = eol<0 ? "" : headerText.substring(eol+1);
         }
         // Parse the name value pair
         var colon = line.indexOf(':');
         var name = line.substring(0,colon);
         var value = line.substring(colon+1);
         headers[name] = value;
         eol = headerText.indexOf("\n");
      }
      if (headerText.length>0) {
         var colon = headerText.indexOf(':');
         var name = headerText.substring(0,colon);
         var value = headerText.substring(colon+1);
         headers[name] = value;
      }
   }
   return headers;
}

/**
 * The following keys can be sent:
 * onSuccess (required)  a function called when the response is 2xx
 * onFailure             a function called when the response is not 2xx
 * username              The username for basic auth
 * password              The password for basic auth
 * overrideMimeType      The mime type to use for non-XML response mime types
 * timeout               A timeout value in milliseconds for the response
 * onTimeout             A function to call if the request times out.
 * body                  A string containing the entity body of the request
 * contentType           The content type of the entity body of the request
 * headers               A hash of optional headers
 */
function HTTP(method,url,options)
{

   //alert(method+" "+url+" "+options.username);

   var requester = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
   requester = requester.QueryInterface(Components.interfaces.nsIXMLHttpRequest);
   //var requester = new XMLHttpRequest();

   var timeout = null;
   if (!options.synchronizedRequest) {

      requester.onreadystatechange = function() {
         if (requester.readyState==4) {
            if (timeout) {
               clearTimeout(timeout);
            }
            try {
               if (requester.status) {
               }
            } catch (ex) {
               if (options.onFailure) {
                  options.onFailure(
                     -1,
                     null,
                     ex,
                     null
                  );
               }
               return;
            }
            try {
               if (requester.status==0 || (requester.status>=200 && requester.status<300)) {
                  options.onSuccess(
                     requester.status,
                     requester.responseXML,
                     requester.responseText,
                     options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null
                  );
               } else {
                  if (options.onFailure) {
                     options.onFailure(
                        requester.status,
                        requester.responseXML,
                        requester.responseText,
                        options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null
                     );
                  }
               }
            } catch (ex) {
               if (options.onFailure) {
                  options.onFailure(-1,requester.responseXML,ex);
               }
            }
         }
      }
   }

   if (options.overrideMimeType) {
      requester.overrideMimeType(options.overrideMimeType);
   }
   if (options.username && options.username!="") {
      requester.open(method,url+"",!options.synchronizedRequest,options.username,options.password);
   } else {
      requester.open(method,url+"",!options.synchronizedRequest);
   }
   if (options.timeout && !options.synchronizedRequest) {
      timeout = setTimeout(
          function() {
             var callback = options.onTimeout ? options.onTimeout : options.onFailure;
             callback(0,"Operation timeout.");
          },
          options.timeout
      );
   }
   if (options.headers) {
      for (var name in options.headers) {
         requester.setRequestHeader(name,options.headers[name]);
      }
   }
   if (options.body) {
      requester.setRequestHeader("Content-Type",options.contentType);
      requester.send(options.body);
   } else {
      requester.send(null);
   }
   if (options.synchronizedRequest) {
      if (requester.status==0 || (requester.status>=200 && requester.status<300)) {
         options.onSuccess(
            requester.status,
            requester.responseXML,
            requester.responseText,
            options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null
         );
      } else {
         if (options.onFailure) {
            options.onFailure(
               requester.status,
               requester.responseXML,
               requester.responseText,
               options.returnHeaders ? _HTTP_parseHeaders(requester.getAllResponseHeaders()) : null
            );
         }
      }
      return {
         abort: function() {
         }
      };
   } else {
      return {
         abort: function() {
            clearTimeout(timeout);
            requester.abort();
         }
      };
   }
}

// END HTTP.js

// START URI.js
/*
 *
Copyright (c) 2008, R. Alexander Milowski
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

 - Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 - Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 - Neither the name of the R. Alexander Milowski nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 *
 */

function URI(spec) {
   this.spec = spec;
   this.parse();
}

URI.prototype.parse = function() {
   var pos = 0;
   for (; pos<this.spec.length && this.spec.charAt(pos)!=':'; pos++);
   if (pos==this.spec.length) {
      throw "Bad URI value, no scheme: "+this.spec;
   }
   this.scheme = this.spec.substring(0,pos);
   this.schemeSpecificPart = this.spec.substring(pos+1);
   pos++;
   if (this.spec.charAt(pos)=='/' && this.spec.charAt(pos+1)=='/') {
      this.parseGeneric();
   }
}

URI.prototype.parseGeneric = function() {
   if (this.schemeSpecificPart.charAt(0)!='/' || this.schemeSpecificPart.charAt(1)!='/') {
      throw "Generic URI values should start with '//':"+this.spec;
   }

   var work = this.schemeSpecificPart.substring(2);
   var pathStart = work.indexOf("/");
   if (pathStart<0) {
      throw "There must be a server specification: "+this.spec;
   }
   this.authority = work.substring(0,pathStart);
   this.path = work.substring(pathStart);
   var hash = this.path.indexOf('#');
   if (hash>=0) {
      this.fragment = this.path.substring(hash+1);
      this.path = this.path.substring(0,hash);
   }
   var questionMark = this.path.indexOf('?');
   if (questionMark>=0) {
      this.query = this.path.substring(questionMark+1);
      this.path = this.path.substring(0,questionMark);
   }
   this.segments = this.path.split(/\//);
   if (this.segments.length>0 && this.segments[0]=='' && this.path.length>1 && this.path.charAt(1)!='/') {
      // empty segment at the start, remove it
      this.segments.shift();
   }
   if (this.segments.length>0 && this.path.length>0 && this.path.charAt(this.path.length-1)=='/' && this.segments[this.segments.length-1]=='') {
      // we may have an empty the start
      // check to see if it is legimate
      if (this.path.length>1 && this.path.charAt(this.path.length-2)!='/') {
         this.segments.pop();
      }
   }
   this.isGeneric = true;
}

URI.prototype.resolve = function(href) {
   if (!href) {
      return new URI(this.spec);
   }
   if (!this.isGeneric) {
      throw "Cannot resolve uri against non-generic URI: "+this.spec;
   }
   if (href.charAt(0)=='/') {
      return new URI(this.scheme+"://"+this.authority+href);
   } else if (href.charAt(0)!='#') {
      if (href.charAt(0)=='.' && href.charAt(1)=='/') {
         href = href.substring(1);
      }
      if (this.path.charAt(this.path.length-1)=='/') {
         return new URI(this.scheme+"://"+this.authority+this.path+href.substring(2));
      } else {
         var last = this.path.lastIndexOf('/');
         return new URI(this.scheme+"://"+this.authority+this.path.substring(0,last+1)+href);
      }
   } else {
      return new URI(this.scheme+"://"+this.authority+this.path+href);
   }
}

URI.prototype.relativeTo = function(otherURI) {
   if (otherURI.scheme!=this.scheme) {
      return this.spec;
   }
   if (!this.isGeneric) {
      throw "A non generic URI cannot be made relative: "+this.spec;
   }
   if (!otherURI.isGeneric) {
      throw "Cannot make a relative URI against a non-generic URI: "+otherURI.spec;
   }
   if (otherURI.authority!=this.authority) {
      return this.spec;
   }
   for (var i=0; i<this.segments.length && i<otherURI.segments.length; i++) {
      if (this.segments[i]!=otherURI.segments[i]) {
         //alert(this.path+" different from "+otherURI.path+" at '"+this.segments[i]+"' vs '"+otherURI.segments[i]+"'");
         var relative = "";
         for (var j=i; j<otherURI.segments.length; j++) {
            relative += "../";
         }
         for (var j=i; j<this.segments.length; j++) {
            relative += this.segments[j];
            if ((j+1)<this.segments.length) {
               relative += "/";
            }
         }
         if (this.path.charAt(this.path.length-1)=='/') {
            relative += "/";
         }
         return relative;
      }
   }
   if (this.segments.length==otherURI.segments.length) {
      return hash ? hash : (query ? query : "");
   } else if (i<this.segments.length) {
      var relative = "";
      for (var j=i; j<this.segments.length; j++) {
         relative += this.segments[j];
         if ((j+1)<this.segments.length) {
            relative += "/";
         }
      }
      if (this.path.charAt(this.path.length-1)=='/') {
         relative += "/";
      }
      return relative;
   } else {
      throw "Cannot calculate a relative URI for "+this.spec+" against "+otherURI.spec;
   }
}
// END URI.js

Console = {
   appName: "XMLApp",
   preferencesService: Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService).getBranch("xmlapp."),
   _initialized: false,
   _debug: false,
   service: Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService),
   log: function(message) {
      this.service.logStringMessage(this.appName+": "+message);
   },
   debug: function(message) {
      if (!this._initialized) {
         if (Console.preferencesService) {
            try {
               Console._debug = Console.preferencesService.getBoolPref("debug");
            } catch (ex) {
               // no preference
            }
         }
         this._initialized = true;
      }
      if (this._debug) {
         this.service.logStringMessage(this.appName+": "+message);
      }
   }
}

var XMLAPP_NS = "http://www.milowski.com/Vocabulary/XMLApp/";
var EXT_ID = "{6a545900-e51a-493a-8682-e246b64fe728}";


function trim(s) {
   return s.replace(/^\s*/, "").replace(/\s*$/, "");
}

function DOMContentHandler() {
   this.level = -1;
   this.errors = 0;
}

DOMContentHandler.prototype.QueryInterface =  function(iid) {
   if (iid.equals(Components.interfaces.nsISAXContentHandler) || iid.equals(Components.interfaces.nsISAXErrorHandler) || iid.equals(Components.interfaces.nsISupports)) {
      return this;
   }
   throw Components.results.NS_ERROR_NO_INTERFACE;
}

DOMContentHandler.prototype.startDocument = function() {
   this.level++;
   this.prefixes = {};
   try {
      this.document = Components.classes["@mozilla.org/xml/xml-document;1"].createInstance(Components.interfaces.nsIDOMXMLDocument);
      this.parent = this.document;
   } catch (ex) {
      Console.log(ex);
   }
}

DOMContentHandler.prototype.endDocument = function() {
   this.level--;
}


DOMContentHandler.prototype.startElement = function(ns ,localName ,qName ,attributes) {

   //Console.log("start {"+ns+"}"+localName);
   this.level++;

   try {
      var child = this.document.createElementNS(ns,qName);
      this.parent.appendChild(child);
      this.parent = child;
      for (var i=0; i<attributes.length; i++) {
         var attQName = attributes.getQName(i);
         var attValue = attributes.getValue(i);
         var attNS    = attributes.getURI(i);
         if (attNS) {
            this.parent.setAttributeNS(attNS,attQName,attValue);
         } else {
            this.parent.setAttribute(attQName,attValue);
         }
      }
   } catch (ex) {
      Console.log(ex);
   }

   for (var prefix in this.prefixes) {
      var uri = this.prefixes[prefix];
      try {
         if (!prefix || prefix.length==0) {
            this.parent.setAttribute("xmlns",uri);
         } else {
            this.parent.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:"+prefix,uri);
         }
      } catch (ex) {
         Console.log(ex);
      }
   }
   
   this.prefixes = {};
}

DOMContentHandler.prototype.endElement = function(ns ,localName ,qName) {
   //Console.log("end {"+ns+"}"+localName);
   this.level--;

   try {
      this.parent = this.parent.parentNode;
   } catch (ex) {
      Console.log(ex);
   }
}

DOMContentHandler.prototype.startPrefixMapping = function(prefix ,uri) {
   this.prefixes[prefix] = uri;
}

DOMContentHandler.prototype.endPrefixMapping = function(prefix) {
}

DOMContentHandler.prototype.characters = function(value) {
   if (this.level>0) {
      try {
         this.parent.appendChild(this.document.createTextNode(value));
      } catch (ex) {
         Console.log(ex);
      }
   }
}

DOMContentHandler.prototype.ignorableWhitespace = function(whitespace) {
   if (this.level>0) {
      try {
         this.parent.appendChild(this.document.createTextNode(whitespace));
      } catch (ex) {
         Console.log(ex);
      }
   }
}

DOMContentHandler.prototype.processingInstruction = function(target ,data) {
   //if (this.level>0) {
      try {
         this.parent.appendChild(this.document.createProcessingInstruction(target,data));
      } catch (ex) {
         Console.log(ex);
      }
   //}
}


DOMContentHandler.prototype.error = function(locator ,error ) {
   Console.log(error);
   this.errors++;
   throw error;
}

DOMContentHandler.prototype.fatalError = function(locator , error ) {
   Console.log(error);
   this.errors++;
   throw error;
}

DOMContentHandler.prototype.ignorableWarning = function(locator , error ) {
   Console.log(error);
}

function XMLConverter(type) {
   this.handler = new DOMContentHandler();
   this.sourceType = type;
   this.reader = Components.classes["@mozilla.org/saxparser/xmlreader;1"].createInstance(Components.interfaces.nsISAXXMLReader);
   this.reader.contentHandler = this.handler;
   this.reader.errorHandler = this.handler;
}

XMLConverter.prototype.QueryInterface = function(iid) {
    if (iid.equals(Components.interfaces.nsIStreamConverter) ||
        iid.equals(Components.interfaces.nsIStreamListener) ||
        iid.equals(Components.interfaces.nsIRequestObserver)||
        iid.equals(Components.interfaces.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
}

XMLConverter.prototype.parseParameters = function(type) {
   
   var parts = type.split(";");
   this.parameters = {};
   for (var i=1; i<parts.length; i++) {
      var part = trim(parts[i]);
      var eq = part.indexOf("=");
      var name = part.substring(0,eq);
      var value = part.substring(eq+1);
      if (value.charAt(0)=='"' || value.charAt(0)=="'") {
         // trim the quotes
         value = value.substring(1);
         value = value.substring(0,value.length-1);
      }
      var values = this.parameters[name];
      if (!values) {
         values = [];
         this.parameters[name] = values;
      }
      values.push(decodeURIComponent(value));
   }
   
}

/**
* See nsIRequestObserver.idl
*/
XMLConverter.prototype.onStartRequest = function(request, context) {
   var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                   .getService(Components.interfaces.nsIWindowMediator);
   var mainWindow = wm.getMostRecentWindow("navigator:browser");
   this.browser = mainWindow.getBrowser();

   this.request = request;
   this.context = context;
   try {
      var channel = request.QueryInterface(Components.interfaces.nsIChannel);
      if (channel.contentCharset) {
         Console.debug("Channel has charset property: "+channel.contentCharset)
         this.charset = channel.contentCharset;
      }
      this.uri = channel.URI;
      channel.contentType = this.sourceType;
   } catch (ex) {

   }
   if (this.uri) {
      this.reader.baseURI = this.uri;
   }
   this.reader.parseAsync(null);
   this.reader.onStartRequest(request,context);

   Console.debug(this.sourceType+": Start.");
}

/**
* See nsIRequestObserver.idl
*/
XMLConverter.prototype.onStopRequest = function(request, context, status) {
   try {
      this.reader.onStopRequest(request,context,status);
   } finally {
      this.reader = null;
   }
   Console.debug(this.sourceType+": Stop, status="+status);
   this.status = status;
   this.onComplete();
}

/**
* See nsIStreamListener.idl
*/
XMLConverter.prototype.onDataAvailable = function(request, context, inputStream,sourceOffset, count) {
  this.reader.onDataAvailable(request,context,inputStream,sourceOffset,count);
}

/**
* See nsIStreamConverter.idl
*/
XMLConverter.prototype.asyncConvertData = function(sourceType, destinationType, listener, context) {
   this.parseParameters(sourceType);
   this.listener = listener;
   Console.debug(this.sourceType+": asyncConvertData "+sourceType+", charset="+this.charset);
}

/**
* See nsIStreamConverter.idl
*/
XMLConverter.prototype.convert = function(sourceStream, sourceType, destinationType,context) {
   this.parseParameters(sourceType);
   this.reader.parseFromStream(sourceStream,this.charset,"application/xml");
   Console.debug(this.sourceType+": convert "+sourceType+", charset="+this.charset+", data.length="+this.data.length);
}

/**
* See nsIStreamConverter.idl
*/
XMLConverter.prototype.canConvert = function(sourceType, destinationType) {
   Console.debug(this.sourceType+": canConvert "+sourceType+" -> "+destinationType);
   // We only support one conversion.
   return destinationType == TYPE_ANY && sourceType==this.sourceType;
}

XMLConverter.prototype.onComplete = function() {
   // TODO: handle errors?
   if (this.handler.errors>0) {
      Console.log("XML Application aborted.");
      return;
   }

/*
   try {
      this.handler.document.baseURI = this.uri.spec;
   } catch (ex) {
      Console.log(ex);
   }
   */
   var xmlapp = Components.classes["@milowski.com/xmlapp;1"].getService(Components.interfaces.nsISupports).wrappedJSObject;

   var mediaType = this.sourceType;
   var semicolon = mediaType.indexOf(";");
   if (semicolon>0) {
      mediaType = mediaType.substring(0,semicolon);
      mediaType = trim(mediaType);
   }

   var info = xmlapp.push(this.uri.spec,this.handler.document,mediaType,this.parameters);
   info.app = xmlapp.getApplication(info);

   var extLocation = Components.classes["@mozilla.org/extensions/manager;1"].getService(Components.interfaces.nsIExtensionManager).getInstallLocation(EXT_ID);
   
   if (info.app) {
      var bootstrapType = "html";
      if (info.app.type=="application/xhtml+xml") {
         bootstrapType = "xhtml";
      } else if (info.app.type=="application/xml") {
         bootstrapType = "xml";
      }

      var bootstrapFile = extLocation.getItemFile(EXT_ID,"content/bootstrap."+bootstrapType);
      var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
      var bootstrapURI = ios.newFileURI(bootstrapFile);
      var chromeChannel = ios.newChannelFromURI(bootstrapURI, null);
      chromeChannel.originalURI = this.uri;
      chromeChannel.loadGroup = this.request.loadGroup;
      chromeChannel.asyncOpen(this.listener, null);

   } else {

      // we didn't match but we are here because of a mapped type

      var revertMediaType = "application/xml";

      var typeInfo = xmlapp.mappedTypes[mediaType];
      if (typeInfo) {
         revertMediaType = typeInfo.mediaType;
      }

      // Hack for Firefox's hack for feed subscription
      if (revertMediaType=="application/atom+xml") {
         revertMediaType = "application/vnd.mozilla.maybe.feed";
      }


      // This sort of works ...

      var channel = this.request.QueryInterface(Components.interfaces.nsIChannel);
      channel.contentType = revertMediaType;

      var serializer = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"].createInstance().QueryInterface(Components.interfaces.nsIDOMSerializer);
      var xml = serializer.serializeToString(this.handler.document);

      this.listener.onStartRequest(this.request,this.context);
      var data = Components.classes["@mozilla.org/io/string-input-stream;1"].createInstance(Components.interfaces.nsIStringInputStream);
      data.data = xml;
      this.listener.onDataAvailable(this.request,this.context,data,0,xml.length);
      this.listener.onStopRequest(this.request,this.context,this.status);
      /*
      try {
         var catManager = Components.classes["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager);
         var xmlViewer = catManager.getCategoryEntry("Gecko-Content-Viewers","application/xml");
         var docFactory = Components.classes[xmlViewer].getService(Components.interfaces.nsIDocumentLoaderFactory);
         //var doc = this.handler.document.QueryInterface(Components.interfaces.nsIDocument);
         var doc = this.handler.document;
         var obj = Components.interfaces.nsIDOMXMLDocument;
         for (var id in obj) {
            Console.log(id+"="+obj[id]);
         }
         Console.log("docShell: "+this.browser.docShell+", doc: "+doc+", iface="+Components.interfaces.nsIDOMXMLDocument);
         var viewer = docFactory.createInstanceForDocument(this.browser.docShell,doc,"");
         viewer = viewer.QueryInterface(Components.interfaces.nsIContentViewer);
         viewer.container = this.browser.docShell;
      } catch (ex) {
         Console.log("Error: "+ex);
      }
      */

      /*
      var bootstrapFile = extLocation.getItemFile(EXT_ID,"content/bootstrap.xml");
      var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
      var bootstrapURI = ios.newFileURI(bootstrapFile);
      var chromeChannel = ios.newChannelFromURI(bootstrapURI, null);
      chromeChannel.originalURI = this.uri;
      chromeChannel.loadGroup = this.request.loadGroup;
      chromeChannel.asyncOpen(this.listener, null);
      */

   }

}

function GenericComponentFactory(ctor, params) {
  this._ctor = ctor;
  this._params = params;
}
GenericComponentFactory.prototype = {
  _ctor: null,
  _params: null,

  createInstance: function GCF_createInstance(outer, iid) {
    if (outer != null)
      throw Cr.NS_ERROR_NO_AGGREGATION;
    return (new this._ctor(this._params)).QueryInterface(iid);
  },

  QueryInterface: function GCF_QueryInterface(iid) {
    if (iid.equals(Components.interfaces.nsIFactory) ||
        iid.equals(Components.interfaces.nsISupports))
      return this;
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
};


var CONVERTER_CID = Components.ID("{e5b1835e-9916-48d4-ade4-c6d05a1817e3}");
var CONVERTER_NAME = "XMLApp Converter";

function XMLApp() {
   this.wrappedJSObject = this;
   this.apps = {};
   this.documents = {};

   var current = this;

   HTTP("GET","chrome://xmlapp/content/html-identity.xsl",{
       synchronizedRequest: true,
       overrideMimeType: "text/xml",
       onSuccess: function(status,doc) {
          var xform = Components.classes["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Components.interfaces.nsIXSLTProcessor);
          try {
             xform.importStylesheet(doc);
             current.htmlIdentity = xform;
          } catch (ex) {
             Console.log("Cannot load XSLT: "+ex);
          }
       },
       onFailure: function(status,doc) {
          Console.log("Cannot load XHTML identity XSLT document , status="+status);
       }
    });


   HTTP("GET","chrome://xmlapp/content/xml-identity.xsl",{
       synchronizedRequest: true,
       overrideMimeType: "text/xml",
       onSuccess: function(status,doc) {
          var xform = Components.classes["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Components.interfaces.nsIXSLTProcessor);
          try {
             xform.importStylesheet(doc);
             current.xmlIdentity = xform;
          } catch (ex) {
             Console.log("Cannot load XSLT: "+ex);
          }
       },
       onFailure: function(status,doc) {
          Console.log("Cannot load XHTML identity XSLT document , status="+status);
       }
    });

   var cr = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);

   try {

    var extLocation = Components.classes["@mozilla.org/extensions/manager;1"].getService(Components.interfaces.nsIExtensionManager).getInstallLocation(EXT_ID);
    var appDir = extLocation.getItemFile(EXT_ID,"applications");

    if (appDir.exists()) {

       var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
       var catManager = Components.classes["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager);

       var dirEntries = appDir.directoryEntries;
       while (dirEntries.hasMoreElements()) {
          var file = dirEntries.getNext().QueryInterface(Components.interfaces.nsIFile);
          if (file.isDirectory()) {
             var appFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
             appFile.initWithPath(file.path);
             appFile.append("application.xml");
             if (!appFile.exists()) {
                Console.log("No application.xml in "+file.path);
                continue;
             }
             var url = ios.newFileURI(appFile);

             var name = file.leafName;

             var doc = null;
             HTTP("GET",url.spec, {
                overrideMimeType: "text/xml",
                synchronizedRequest: true,
                onSuccess: function(status,xml,text) {
                   doc = xml;
                },
                onFailure: function(status) {
                   throw "Cannot get document from "+url+", status="+status;
                }
             });

             if (doc) {

                var baseURI = new URI("chrome://xmlapp-apps/content/"+name+"/application.xml");
                
                var info = {
                   name: name+"",
                   type: doc.documentElement.getAttribute("type"),
                   types: [],
                   links: [],
                   scripts: [],
                   transforms: []
                };

                Console.debug("Application: "+info.name);
                this.apps[info.name] = info;
                
                var types = doc.documentElement.getElementsByTagNameNS(XMLAPP_NS,"match");
                for (var i=0; i<types.length; i++) {
                   var typeInfo = {
                      mediaType: types[i].getAttribute("type"),
                      namespace: types[i].getAttribute("namespace"),
                      parameters: {},
                      clauses: []
                   };
                   if (typeInfo.mediaType && typeInfo.mediaType.length>0) {
                      Console.debug("Media type: "+typeInfo.mediaType);
                      info.types.push(typeInfo);
                      var params = types[i].getElementsByTagNameNS(XMLAPP_NS,"parameter");
                      typeInfo.count = params.length;
                      for (var j=0; j<params.length; j++) {
                         var pname = params[j].getAttribute("name");
                         var pvalue = params[j].getAttribute("value");
                         Console.debug("Parameter: "+pname+"="+pvalue);
                         typeInfo.parameters[pname] = pvalue;
                      }
                      var clauses = types[i].getElementsByTagNameNS(XMLAPP_NS,"where");
                      for (var j=0; j<clauses.length; j++) {
                         var xpath = clauses[j].getAttribute("select");
                         Console.debug("where clause: "+xpath);
                         typeInfo.clauses.push({
                            context: clauses[j],
                            where: xpath
                         });
                      }
                   }
                }

                var maps = doc.documentElement.getElementsByTagNameNS(XMLAPP_NS,"map");
                for (var i=0; i<maps.length; i++) {
                   var extension = maps[i].getAttribute("extension");
                   var mediaType = maps[i].getAttribute("type");
                   Console.debug("Mapping "+extension+" -> "+mediaType);
                   catManager.addCategoryEntry("ext-to-type-mapping",extension,mediaType,false,true);
                }
                var links = doc.documentElement.getElementsByTagNameNS(XMLAPP_NS,"link");
                for (var i=0; i<links.length; i++) {
                   var rel = links[i].getAttribute("rel");
                   var type = links[i].getAttribute("type");
                   var href = links[i].getAttribute("href");
                   info.links.push({ rel: rel, type: type, href: href});
                }
                var scripts = doc.documentElement.getElementsByTagNameNS(XMLAPP_NS,"script");
                for (var i=0; i<scripts.length; i++) {
                   var privileged = scripts[i].getAttribute("privileged")=="true";
                   info.scripts.push({ type: scripts[i].getAttribute("type"), src: scripts[i].getAttribute("src"), privileged: privileged});
                }
                var xforms = doc.documentElement.getElementsByTagNameNS(XMLAPP_NS,"transform");
                for (var i=0; i<xforms.length; i++) {

                   var src = xforms[i].getAttribute("src");
                   var role = xforms[i].getAttribute("role");

                   var uri = baseURI.resolve(src);
                   Console.debug("Transform "+role+" from "+uri.spec+" -> "+mediaType);

                   HTTP("GET",uri.spec,{
                      synchronizedRequest: true,
                      overrideMimeType: "text/xml",
                      onSuccess: function(status,doc) {
                         //Console.log(doc.documentElement.namespaceURI+", status="+status);
                         var xform = Components.classes["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Components.interfaces.nsIXSLTProcessor);
                         //Console.log("XSLT flags: "+xform.flags);
                         try {
                            xform.importStylesheet(doc);
                            info.transforms.push({ uri: uri.spec, xform: xform, mediaType: mediaType, role: role});
                         } catch (ex) {
                            Console.log("Cannot load XSLT: "+ex);
                         }
                      },
                      onFailure: function(status,doc) {
                         Console.log("Cannot load XSLT document from "+uri.spec+", status="+status);
                      }
                   });

                }
             }

          }
       }



    }

    // index by media type
    this.types = {};
    this.mappedTypes = {};
    for (var name in this.apps) {
       var app = this.apps[name];
       Console.debug("Indexing "+app.name);
       for (var i=0; i<app.types.length; i++) {
          var mediaType = app.types[i].mediaType;
          var typeInfo = this.types[mediaType];
          if (!typeInfo) {
             typeInfo = {
                mediaType: mediaType,
                apps: [],
                inspect: false
             };
             this.types[mediaType] = typeInfo;
          }
          if (app.types[i].namespace || app.types[i].count>0 || app.types[i].clauses.length>0) {
             typeInfo.inspect = true;
             var slash = mediaType.indexOf("/");
             mediaType = mediaType.substring(0,slash+1)+"x-milowski-"+mediaType.substring(slash+1);
             typeInfo.mappedType = mediaType;
             this.mappedTypes[typeInfo.mappedType] = typeInfo;
          }
          typeInfo.apps.push(app);
       }
    }

    // add converter
    var converterPrefix = "@mozilla.org/streamconv;1?from=";
    for (var mediaType in this.types) {
       var typeInfo = this.types[mediaType];
       if (typeInfo.inspect) {
          mediaType = typeInfo.mappedType;
       }
       var contractId = converterPrefix + mediaType+"&to=*/*";
       Console.debug("Adding converter: "+contractId);
       cr.registerFactory(CONVERTER_CID,CONVERTER_NAME,contractId,new GenericComponentFactory(XMLConverter,[mediaType]));
    }

    } catch (ex) {
       Console.log("Error in loading extension: "+ex);
    }

}

XMLApp.prototype.QueryInterface = function(iid) {
   if (!iid.equals(Components.interfaces.nsISupports)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
   }
   return this;
}

XMLApp.prototype.push = function(uri,document,type,parameters) {
   var info = {
      uri: uri,
      document: document,
      type: type,
      parameters: parameters
   };
   this.documents[uri] = info;
   return info;
}

XMLApp.prototype.pop = function(uri) {
   var info = this.documents[uri];
   delete this.documents[uri];
   return info;
}

XMLApp.prototype.release = function(uri) {
}

XMLApp.prototype.getApplication = function(info) {
   try {
   var typeInfo = this.types[info.type];
   if (!typeInfo) {
      typeInfo = this.mappedTypes[info.type];
   }
   if (!typeInfo) {
      Console.debug("Nothing registered for "+info.type);
      return null;
   }
   var matches = [];
   Console.debug("Candidates: "+typeInfo.apps.length);
   for (var i=0; i<typeInfo.apps.length; i++) {
      var candidate = typeInfo.apps[i];
      var mediaType = info.type;
      if (typeInfo.mappedType) {
         mediaType = typeInfo.mediaType;
      }
      for (var t=0; t<candidate.types.length; t++) {
         if (candidate.types[t].mediaType==mediaType) {
            Console.debug("Processing candidate for "+mediaType);
            if (candidate.types[t].namespace && info.document.documentElement.namespaceURI!=candidate.types[t].namespace) {
               continue;
            }
            var parametersMatched = true;
            for (var name in candidate.parameters) {
               var cvalue = candidate.parameters[name];
               var values = info.parameters[name];
               var found = false;
               if (values) {
                  for (var v=0; !found && v<values.length; v++) {
                     if (values[v]==cvalue) {
                        found = true;
                     }
                  }
               }
               if (!found) {
                  parametersMatched = false;
                  break;
               }
            }
            if (parametersMatched) {
               matches.push({ app: candidate, type: candidate.types[t] });
            }
         }
      }
   }

   Console.debug("Checking clauses...");

   for (var i=0; i<matches.length; i++) {
      if (matches[i].type.clauses.length>0) {
         var hasClause = true;
         var type = matches[i].type;
         for (var j=0; hasClause && j<type.clauses.length; j++) {
            Console.debug(j+": "+type.clauses[j].where);
            var nsResolver = type.clauses[j].context.ownerDocument.createNSResolver(type.clauses[j].context);
            var result = info.document.evaluate(type.clauses[j].where,info.document,nsResolver,0,null);
            hasClause = result.iterateNext();
         }
         if (!hasClause) {
            // remove match
            matches[i] = null;
         }
      }
   }

   Console.debug("Sorting matches...");

   var match = null;
   for (var i=0; i<matches.length; i++) {
      if (!matches[i]) {
         continue;
      }
      if (match) {
         if (match.type.count<matches[i].type.count) {
            match = matches[i];
         }
      } else {
         match = matches[i];
      }
   }
   return match ? match.app : null;
   } catch (ex) {
      Console.log(ex);
      return null;
   }
}


var Module = {
   CID:  Components.ID("{2a28d313-d1fe-44a0-bddd-614ed81c2bbe}"),
   PROGID: "@milowski.com/xmlapp;1",
   instance: null,
   factory:  {
      getService: function(outer,iid) {
         if (outer != null) {
            throw Components.results.NS_ERROR_NO_AGGREGATION;
         }
         if (!this.instance) {
            this.instance = new XMLApp();
         }
         return this.instance.QueryInterface(iid);
      },
      createInstance: function (outer, iid) {
         if (outer != null) {
            throw Components.results.NS_ERROR_NO_AGGREGATION;
         }
         return (new XMLApp()).QueryInterface(iid);
      }
   },

   QueryInterface: function M_QueryInterface(iid) {
      if (iid.equals(Components.interfaces.nsIModule) || iid.equals(Components.interfaces.nsISupports)) {
         return this;
      }
      throw Components.results.NS_ERROR_NO_INTERFACE;
   },

   getClassObject: function M_getClassObject(cm, cid, iid) {
      if (!iid.equals(Components.interfaces.nsIFactory)) {
         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      }

      if (cid.equals(this.CID)) {
         return this.factory
      }

      throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  registerSelf: function M_registerSelf(cm, file, loader, type) {

     //Console.log("File: "+file.path);
     //Console.log("Loader: "+loader);
     //Console.log("Type: "+type);

     var cr = cm.QueryInterface(Components.interfaces.nsIComponentRegistrar);
     cr.registerFactoryLocation(this.CID,
                                     "XMLApp Service Component",
                                     this.PROGID,
                                     file,
                                     loader,
                                     type);



  },

  unregisterSelf: function M_unregisterSelf(cm, location, type) {
  },

  canUnload: function M_canUnload(cm) {
    return true;
  }
};

function NSGetModule(cm, file) {
  return Module;
}
